﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;

namespace CSLive.ControlPanel.Editor
{
    /// <summary>
    /// The  Colorizer State machine
    /// Used to colorize the editor controls as HTML
    /// GPC is available from http://www.codeproject.com/KB/cs/GPC.aspx
    /// </summary>
    public class Colorizer
    {
        #region Delegates

        public delegate void PieceColorizedEventHandler( object sender, PieceColorizedEventArgs e );

        #endregion

        /// <summary>
        /// The set of current states. The first entry in the list is the main state
        /// </summary>
        private readonly List< State > _current = new List< State >();

        /// <summary>
        /// A lookup of all available states indexed by name
        /// </summary>
        private readonly Dictionary< string, State > _states = new Dictionary< string, State >();

        /// <summary>
        /// The state stack (See Push and Pop rule attributes)
        /// </summary>
        private readonly Stack< State > _stateStack = new Stack< State >();

        /// <summary>
        /// The position we have reached on the current line being parsed
        /// </summary>
        private int _index;

        /// <summary>
        /// Is a colorization span currently open?
        /// </summary>
        private bool _spanOpen;

        /// <summary>
        /// Construct the colorization state machine using the specified rules
        /// </summary>
        /// <param name="rulePath">The path to the xml rules file</param>
        public Colorizer( string rulePath )
        {
            using ( XmlReader xr = new XmlTextReader( rulePath ) )
            {
                while ( xr.Read() )
                    if ( xr.IsStartElement() && xr.Name == "rule" )
                        addRule( xr );
            }
        }

        /// <summary>
        /// Return the active states as a space separated list of names.
        /// This can then be applied as a css class to a span
        /// </summary>
        private string currentStateList
        {
            get
            {
                string rc = "";
                foreach ( State state in _current )
                {
                    if ( rc.Length > 0 ) rc += " ";
                    rc += state.Name;
                }
                return rc;
            }
        }

        /// <summary>
        /// Add an individual rule from the file
        /// </summary>
        /// <param name="xr">The input stream</param>
        private void addRule( XmlReader xr )
        {
            foreach ( string from in xr.GetAttribute( "from" ).Split( ',' ) )
            {
                var rule = new Rule();
                rule.From = getStateProvider( from );
                rule.Remove = ( xr.GetAttribute( "remove" ) == "true" );
                rule.Pop = ( xr.GetAttribute( "pop" ) == "true" );
                rule.Debug = ( xr.GetAttribute( "debug" ) == "true" );
                rule.To = getStateProvider( xr.GetAttribute( "to" ) );
                rule.Transient = getStateProvider( xr.GetAttribute( "transient" ) );
                rule.Push = getStateProvider( xr.GetAttribute( "push" ) );
                rule.Add = getStateProvider( xr.GetAttribute( "add" ) );
                rule.Pattern = xr.GetAttribute( "pattern" );

                State state = findState( from );
                state.Rules.Add( rule );
            }
        }

        /// <summary>
        /// Construct a state from an attribute value
        /// </summary>
        /// <param name="attribValue">A state name or a choice list</param>
        /// <returns>A state- found or newly created</returns>
        private IStateProvider getStateProvider( string attribValue )
        {
            if ( String.IsNullOrEmpty( attribValue ) )
                return null;
            if ( attribValue.StartsWith( "=" ) )
                return new StateChoice( this, attribValue.Substring( 1 ) );
            return findState( attribValue );
        }

        /// <summary>
        /// Find the state with the specified name or create it if it doesn't already exist
        /// </summary>
        /// <param name="stateName"></param>
        /// <returns></returns>
        private State findState( string stateName )
        {
            State rc = null;
            if ( !String.IsNullOrEmpty( stateName ) )
            {
                if ( !_states.TryGetValue( stateName, out rc ) )
                {
                    rc = new State();
                    rc.Name = stateName;
                    _states.Add( stateName, rc );
                }
            }
            return rc;
        }

        /// <summary>
        /// The PieceColorized Event is triggered after each matched piece of input is colorized. You can attach to this event to perform
        /// additional pretty formatting e.g. Indenting of XML
        /// </summary>
        public event PieceColorizedEventHandler PieceColorized;

        /// <summary>
        /// Colorize an individual matched piece
        /// </summary>
        /// <param name="piece">The input piece of text</param>
        /// <returns>The colorized piece</returns>
        private string colorizePiece( string piece )
        {
            string rc = _spanOpen ? "</span>" : "";
            string className = currentStateList;
            if ( className.IndexOf( "noemit" ) < 0 )
            {
                rc += String.Format( "<span class=\"{0}\">{1}", className, piece.Replace( "<", "&lt;" ).Replace( ">", "&gt;" ).Replace( " ", "&nbsp;" ) );
                _spanOpen = true;
            }

            if ( PieceColorized != null )
            {
                var evt = new PieceColorizedEventArgs( _current[ 0 ].Name, piece, rc );
                PieceColorized( this, evt );
                rc = evt.ColorizedPiece;
            }
            return rc;
        }

        /// <summary>
        /// Get the next match on this line
        /// </summary>
        /// <param name="line">The input line</param>
        /// <param name="si">The current index from which to start</param>
        /// <param name="match">The resulting match if any</param>
        /// <returns>true if a match is found</returns>
        private bool getNextMatch( string line, out int si, out Match match )
        {
            int lowestIndex = int.MaxValue;
            int longestLength = int.MinValue;
            match = null;
            si = 0;
            for ( int i = 0; i < _current.Count; i++ )
            {
                Regex ex = _current[ i ].Pattern;
                Match m = ex.Match( line, _index );
                if ( m.Success )
                    if ( m.Index < lowestIndex || ( m.Index == lowestIndex && m.Length > longestLength ) )
                    {
                        si = i;
                        match = m;
                        lowestIndex = m.Index;
                        longestLength = m.Length;
                    }
            }
            return ( match != null );
        }

        /// <summary>
        /// Colorize an individual line of text
        /// </summary>
        /// <param name="line">The input line</param>
        /// <returns>The colorized line</returns>
        private string colorizeLine( string line )
        {
            _index = 0;
            string oline = "";
            int loop = 0;
            for ( ;; )
            {
                if ( loop++ > 100 )
                    throw new ColorizerException( string.Format( "Unable to advance at: State={0}, location={1}", currentStateList, line.Substring( _index ) ) );
                int si;
                Match match;
                // Get the next piece of matched text
                if ( getNextMatch( line, out si, out match ) )
                {
                    string lastPiece = "";
                    if ( match.Index > _index )
                    {
                        lastPiece = line.Substring( _index, match.Index - _index );
                        oline += colorizePiece( lastPiece );
                        _index = match.Index;
                        loop = 0;
                    }
                    for ( int g = 1; g < match.Groups.Count; g++ )
                    {
                        // Find out which rule we've matched and execute it
                        if ( match.Groups[ g ].Success )
                        {
                            Rule rule = _current[ si ].Rules[ g - 1 ];
                            if ( rule.Debug )
                                Debugger.Break();
                            if ( rule.Transient != null )
                            {
                                State prevState = _current[ si ];
                                _current[ si ] = rule.Transient.GetState( _current[ si ], lastPiece );
                                oline += colorizePiece( match.Value );
                                _current[ si ] = prevState; // Return to original From State unless there is a To state
                                _index += match.Length;
                            }
                            if ( rule.Push != null )
                                _stateStack.Push( rule.Push.GetState( _current[ si ], lastPiece ) );
                            if ( rule.Pop )
                                _current[ si ] = _stateStack.Pop();
                            if ( rule.To != null )
                                _current[ si ] = rule.To.GetState( _current[ si ], lastPiece );
                            if ( rule.Remove )
                                _current.RemoveAt( si );
                            if ( rule.Add != null )
                                _current.Add( rule.Add.GetState( _current[ si ], lastPiece ) );
                        }
                    }
                }
                else
                    break;
            }
            if ( _index < line.Length )
                oline += colorizePiece( line.Substring( _index ) );
            return oline;
        }

        /// <summary>
        /// Colorize the input stream to produce an output stream
        /// </summary>
        /// <param name="initialStateName">The initial state in which to start the </param>
        /// <param name="reader"></param>
        /// <param name="lines"></param>
        public void Colorize( string initialStateName, TextReader reader, List< string > lines )
        {
            if ( initialStateName == "htm" ) initialStateName = "html";
            State initialState = _states[ initialStateName ];
            if ( initialState == null )
                throw new ColorizerException( string.Format( "Initial State:\"{0}\" does not exist in the rules.", initialStateName ) );
            _current.Clear();
            _current.Add( initialState );
            _spanOpen = false;
            while ( reader.Peek() != -1 )
                lines.Add( colorizeLine( reader.ReadLine() ) );
            if ( _spanOpen )
                lines[ lines.Count - 1 ] += "</span>";
        }

        /// <summary>
        /// Colorize the input stream to produce an output stream
        /// </summary>
        /// <param name="initialStateName">The initial state in which to start the </param>
        /// <param name="line"></param>
        public string ColorizeLine( string initialStateName, string line )
        {
            if ( initialStateName == "htm" ) initialStateName = "html";
            State initialState = _states[ initialStateName ];
            if ( initialState == null )
                throw new ColorizerException( string.Format( "Initial State:\"{0}\" does not exist in the rules.", initialStateName ) );
            _current.Clear();
            _current.Add( initialState );
            _spanOpen = false;
            string s = colorizeLine( line );
            if ( _spanOpen )
                s += "</span>";
            return s;
        }

        #region Nested type: IStateProvider

        /// <summary>
        /// Provides a State
        /// </summary>
        private interface IStateProvider
        {
            /// <summary>
            /// Provide a new state
            /// </summary>
            /// <param name="state">current state</param>
            /// <param name="value">currently matched text</param>
            /// <returns></returns>
            State GetState( State state, string value );
        }

        #endregion

        #region Nested type: Rule

        /// <summary>
        /// An individual rule that can cause a state change
        /// </summary>
        private class Rule
        {
            /// <summary>
            /// Add the specified state to the set of current states
            /// </summary>
            public IStateProvider Add;

            /// <summary>
            /// Invoke the debugger before this rule is executed
            /// </summary>
            public bool Debug;

            /// <summary>
            /// The initial state to which this rule applies
            /// </summary>
            public IStateProvider From;

            /// <summary>
            /// The pattern that must be found for this rule to execute
            /// </summary>
            public string Pattern;

            /// <summary>
            /// Pop a state from the stack 
            /// </summary>
            public bool Pop;

            /// <summary>
            /// Push the specified state on the stack
            /// </summary>
            public IStateProvider Push;

            /// <summary>
            /// Remove this rules initial state from the set of current states
            /// </summary>
            public bool Remove;

            /// <summary>
            /// The final state when this rule completes its transition
            /// </summary>
            public IStateProvider To;

            /// <summary>
            /// The state that exists during this rule's transition
            /// </summary>
            public IStateProvider Transient;
        }

        #endregion

        #region Nested type: State

        private class State : IStateProvider
        {
            // The name of this state
            // The list of rules that can leave this state
            public readonly List< Rule > Rules = new List< Rule >();
            // A combined pattern with a seperate group for each rule
            private Regex _pattern;
            public string Name;

            /// <summary>
            /// Return the combined pattern if it has already being calculated or calculate it from scratch if not
            /// </summary>
            public Regex Pattern
            {
                get
                {
                    if ( _pattern == null )
                    {
                        string regex = "";
                        foreach ( Rule rule in Rules )
                        {
                            if ( regex.Length > 0 )
                                regex += "|";
                            regex += "(" + rule.Pattern + ")";
                        }
                        _pattern = new Regex( regex, RegexOptions.Compiled );
                    }
                    return _pattern;
                }
            }

            #region IStateProvider Members

            /// <summary>
            /// A trivial implementation that provides ourselves as the state
            /// </summary>
            /// <param name="state">not used here</param>
            /// <param name="value">not used here</param>
            /// <returns></returns>
            public State GetState( State state, string value )
            {
                return this;
            }

            #endregion
        }

        #endregion

        #region Nested type: StateChoice

        /// <summary>
        /// The equivalent of a "switch" statement, return one of a selection of states
        /// depending on the currently matched text.
        /// </summary>
        private class StateChoice : IStateProvider
        {
            /// <summary>
            /// Lookup the state indexed by matched text
            /// </summary>
            private readonly Dictionary< string, State > _choiceLookup = new Dictionary< string, State >();

            /// <summary>
            /// The default state to use if the matched text doesn't exist in the lookup
            /// </summary>
            private readonly State _defaultState;

            /// <summary>
            /// Construct a State Choice
            /// </summary>
            /// <param name="colorizer">The Colorizer state machine</param>
            /// <param name="choiceList">Some syntax from which to make the StateChoice.
            /// For example: "=style:css,script:js,html"
            public StateChoice( Colorizer colorizer, string choiceList )
            {
                string[] choices = choiceList.Split( ',' );
                _defaultState = colorizer.findState( choices[ choices.Length - 1 ] );
                for ( int i = 0; i < choices.Length - 1; i++ )
                {
                    string[] part = choices[ i ].Split( ':' );
                    _choiceLookup[ part[ 0 ] ] = colorizer.findState( part[ 1 ] );
                }
            }

            /// <summary>
            /// Return a new State based on the currently matched text
            /// </summary>
            /// <param name="state">Not used</param>
            /// <param name="value">The currently matched text</param>
            /// <returns></returns>

            #region IStateProvider Members

            public State GetState( State state, string value )
            {
                State rc = null;
                if ( !_choiceLookup.TryGetValue( value, out rc ) )
                    rc = _defaultState;
                return rc;
            }

            #endregion
        }

        #endregion
    }

    /// <summary>
    /// The exception to throw if something goes wrong
    /// </summary>
    public class ColorizerException : ApplicationException
    {
        internal ColorizerException( string message ) : base( message )
        {
        }
    }

    /// <summary>
    /// The Arguments when the PieceColorized event is thrown
    /// </summary>
    public class PieceColorizedEventArgs : EventArgs
    {
        /// <summary>
        /// Construct a PieceColorized event argument
        /// </summary>
        /// <param name="mainStateName">The name of the primary state (as opposed to any states that have been "added"</param>
        /// <param name="piece">The piece of text that has just been colorized</param>
        /// <param name="colorizedPiece">The result of the colorization</param>
        public PieceColorizedEventArgs( string mainStateName, string piece, string colorizedPiece )
        {
            MainStateName = mainStateName;
            Piece = piece;
            ColorizedPiece = colorizedPiece;
        }

        /// <summary>
        /// The name of the primary state (as opposed to any states that have been "added"
        /// </summary>
        public string MainStateName { get; private set; }

        // The piece of text that has just been colorized
        public string Piece { get; private set; }

        /// <summary>
        /// The result of the colorization
        /// </summary>
        public string ColorizedPiece { get; set; }
    }
}